/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ArkMethod, FunctionType, Local, MethodSignature, Stmt, Value } from 'arkanalyzer';
import { BaseChecker, BaseMetaData } from '../BaseChecker';
import { CheckerUtils, Defects, MatcherCallback, MatcherTypes, MethodMatcher, Rule } from '../../Index';
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { IssueReport } from '../../model/Defects';

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'BanCallbackOperationsCheck');
const SIGNATURE: string[] = [
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.CameraInput.on('error', @ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.CameraDevice, @ohosSdk/api/@ohos.base.d.ts: ErrorCallback<@ohosSdk/api/@ohos.base.d.ts: BusinessError<void>>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.CameraInput.off('error', @ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.CameraDevice, @ohosSdk/api/@ohos.base.d.ts: ErrorCallback<@ohosSdk/api/@ohos.base.d.ts: BusinessError<void>>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.CameraManager.on('cameraStatus', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.CameraStatusInfo,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.CameraManager.off('cameraStatus', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.CameraStatusInfo,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.CameraManager.on('foldStatusChange', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.FoldStatusInfo,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.CameraManager.off('foldStatusChange', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.FoldStatusInfo,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.MetadataOutput.on('metadataObjectsAvailable', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.MetadataObject[],void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.MetadataOutput.off('metadataObjectsAvailable', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.MetadataObject[],void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.MetadataOutput.on('error', @ohosSdk/api/@ohos.base.d.ts: ErrorCallback<@ohosSdk/api/@ohos.base.d.ts: BusinessError<void>>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.MetadataOutput.off('error', @ohosSdk/api/@ohos.base.d.ts: ErrorCallback<@ohosSdk/api/@ohos.base.d.ts: BusinessError<void>>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.on('photoAvailable', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.Photo,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.off('photoAvailable', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.Photo,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.on('captureStartWithInfo', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.CaptureStartInfo,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.off('captureStartWithInfo', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.CaptureStartInfo,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.on('photoAssetAvailable', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.file.photoAccessHelper.d.ts: photoAccessHelper.PhotoAsset,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.off('photoAssetAvailable', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.file.photoAccessHelper.d.ts: photoAccessHelper.PhotoAsset,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.on('frameShutter', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.FrameShutterInfo,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.off('frameShutter', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.FrameShutterInfo,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.on('captureEnd', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.CaptureEndInfo,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.off('captureEnd', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.CaptureEndInfo,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.on('frameShutterEnd', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.FrameShutterEndInfo,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.off('frameShutterEnd', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.FrameShutterEndInfo,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.on('captureReady', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<void,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.off('captureReady', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<void,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.on('estimatedCaptureDuration', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<number,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.off('estimatedCaptureDuration', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<number,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.on('error', @ohosSdk/api/@ohos.base.d.ts: ErrorCallback<@ohosSdk/api/@ohos.base.d.ts: BusinessError<void>>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.off('error', @ohosSdk/api/@ohos.base.d.ts: ErrorCallback<@ohosSdk/api/@ohos.base.d.ts: BusinessError<void>>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.on('captureStart', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<number,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoOutput.off('captureStart', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<number,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoSession.on('error', @ohosSdk/api/@ohos.base.d.ts: ErrorCallback<@ohosSdk/api/@ohos.base.d.ts: BusinessError<void>>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoSession.off('error', @ohosSdk/api/@ohos.base.d.ts: ErrorCallback<@ohosSdk/api/@ohos.base.d.ts: BusinessError<void>>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoSession.on('focusStateChange', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.FocusState,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoSession.off('focusStateChange', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.FocusState,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoSession.on('smoothZoomInfoAvailable', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.SmoothZoomInfo,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoSession.off('smoothZoomInfoAvailable', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.SmoothZoomInfo,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoSession.on('autoDeviceSwitchStatusChange', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.AutoDeviceSwitchStatus,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PhotoSession.off('autoDeviceSwitchStatusChange', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.AutoDeviceSwitchStatus,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PreviewOutput.on('frameStart', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<void,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PreviewOutput.off('frameStart', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<void,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PreviewOutput.on('frameEnd', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<void,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PreviewOutput.off('frameEnd', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<void,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PreviewOutput.on('error', @ohosSdk/api/@ohos.base.d.ts: ErrorCallback<@ohosSdk/api/@ohos.base.d.ts: BusinessError<void>>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.PreviewOutput.off('error', @ohosSdk/api/@ohos.base.d.ts: ErrorCallback<@ohosSdk/api/@ohos.base.d.ts: BusinessError<void>>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.VideoSession.on('error', @ohosSdk/api/@ohos.base.d.ts: ErrorCallback<@ohosSdk/api/@ohos.base.d.ts: BusinessError<void>>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.VideoSession.off('error', @ohosSdk/api/@ohos.base.d.ts: ErrorCallback<@ohosSdk/api/@ohos.base.d.ts: BusinessError<void>>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.VideoSession.on('focusStateChange', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.FocusState,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.VideoSession.off('focusStateChange', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.FocusState,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.VideoOutput.on('frameStart', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<void,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.VideoOutput.off('frameStart', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<void,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.VideoOutput.on('frameEnd', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<void,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.VideoOutput.off('frameEnd', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<void,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.VideoOutput.on('error', @ohosSdk/api/@ohos.base.d.ts: ErrorCallback<@ohosSdk/api/@ohos.base.d.ts: BusinessError<void>>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.VideoOutput.off('error', @ohosSdk/api/@ohos.base.d.ts: ErrorCallback<@ohosSdk/api/@ohos.base.d.ts: BusinessError<void>>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.VideoSession.on('smoothZoomInfoAvailable', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.SmoothZoomInfo,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.VideoSession.off('smoothZoomInfoAvailable', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.SmoothZoomInfo,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.VideoSession.on('autoDeviceSwitchStatusChange', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.AutoDeviceSwitchStatus,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.VideoSession.off('autoDeviceSwitchStatusChange', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.AutoDeviceSwitchStatus,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.VideoSession.on('error', @ohosSdk/api/@ohos.base.d.ts: ErrorCallback<@ohosSdk/api/@ohos.base.d.ts: BusinessError<void>>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.VideoSession.off('error', @ohosSdk/api/@ohos.base.d.ts: ErrorCallback<@ohosSdk/api/@ohos.base.d.ts: BusinessError<void>>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.VideoSession.on('focusStateChange', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.FocusState,void>)`,
    `@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.VideoSession.off('focusStateChange', @ohosSdk/api/@ohos.base.d.ts: AsyncCallback<@ohosSdk/api/@ohos.multimedia.camera.d.ts: camera.FocusState,void>)`,
];
const keyword = 'on';
const gMetaData: BaseMetaData = {
    severity: 3,
    ruleDocPath: 'docs/bad-callbacks-in-camera-on.md',
    description: 'In the callback on interface, it is prohibited to add (by calling the on method) or remove (by calling the off method) callback operations.'
};

interface StackItem {
    method: ArkMethod;
    processed: boolean;
}

export class BadCallbacksInCameraOn implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public issues: IssueReport[] = [];

    private mtdMatcher: MethodMatcher = {
        matcherType: MatcherTypes.METHOD,
    };

    public registerMatchers(): MatcherCallback[] {
        const matchMethodCb: MatcherCallback = {
            matcher: this.mtdMatcher,
            callback: this.check
        };
        return [matchMethodCb];
    }

    public check = (target: ArkMethod): void => {
        const stmts = target.getBody()?.getCfg()?.getStmts() ?? [];
        for (let stmt of stmts) {
            let invokeExpr = CheckerUtils.getInvokeExprFromStmt(stmt);
            if (!invokeExpr) {
                continue;
            }
            const methodSignature = invokeExpr.getMethodSignature();
            const methodName = methodSignature.getMethodSubSignature().getMethodName();
            if (!(SIGNATURE.includes(methodSignature.toString()) && methodName === keyword)) {
                continue;
            }
            const args = invokeExpr.getArgs();
            this.processArgs(target, stmt, args);
        }
    };

    private processArgs(method: ArkMethod, stmt: Stmt, args: Value[]): void {
        for (let arg of args) {
            if (!(arg instanceof Local)) {
                continue;
            }
            const type = arg.getType();
            if (!(type instanceof FunctionType)) {
                continue;
            }
            const scene = method.getDeclaringArkFile().getScene();
            const callbackMethod = scene.getMethod(type.getMethodSignature());
            if (!callbackMethod) {
                continue;
            }
            this.processCallback(callbackMethod, stmt);
        }
    }

    private processCallback(callbackMethod: ArkMethod, reportStmt: Stmt): void {
        let stack: StackItem[] = [];
        let visitedSigns = new Set<MethodSignature>();
        stack.push({ method: callbackMethod, processed: false });
        while (stack.length > 0) {
            const lastElement = stack.pop();
            if (!lastElement) {
                continue;
            }
            if (!lastElement.processed) {
                const methodSignature = lastElement.method.getSignature();
                if (visitedSigns.has(methodSignature)) {
                    continue;
                }
                visitedSigns.add(methodSignature);
                this.processMethod(lastElement.method, stack, reportStmt);
            }
        }
    }

    private processMethod(mtd: ArkMethod, stack: StackItem[], reportStmt: Stmt): void {
        const stmts = mtd.getBody()?.getCfg()?.getStmts() ?? [];
        for (let stmt of stmts) {
            let invokeExpr = CheckerUtils.getInvokeExprFromStmt(stmt);
            if (!invokeExpr) {
                continue;
            }
            const methodSignature = invokeExpr.getMethodSignature();
            if (SIGNATURE.includes(methodSignature.toString())) {
                this.reportIssue(reportStmt);
                return;
            }
            const method = mtd.getDeclaringArkFile().getScene().getMethod(methodSignature);
            if (!method) {
                continue;
            }
            stack.push({ method: method, processed: false });
        }
    }

    private reportIssue(stmt: Stmt): void {
        const text = stmt.getOriginalText();
        if (!text || text.length === 0) {
            logger.debug('Stmt text is empty.');
            return;
        }
        const index = text.indexOf(keyword);
        if (index === -1) {
            return;
        }
        const severity = this.rule.alert ?? this.metaData.severity;
        const originalPosition = stmt.getOriginPositionInfo();
        const lineNum = originalPosition.getLineNo();
        const startColum = originalPosition.getColNo() + index;
        const endColum = startColum + keyword.length - 1;
        const filePath = stmt.getCfg().getDeclaringMethod().getDeclaringArkFile().getFilePath();
        let defects = new Defects(lineNum, startColum, endColum, this.metaData.description, severity, this.rule.ruleId, filePath,
            this.metaData.ruleDocPath, true, false, false);
        this.issues.push(new IssueReport(defects, undefined));
    }
}